package cn.zyq.charging.order.quartz_job;

import cn.zyq.charging.common.pojo.param.GunStatusUpdateParam;
import cn.zyq.charging.order.constant.Constant;
import cn.zyq.charging.order.dao.repository.OrderRepository;
import cn.zyq.charging.order.feign.DeviceClient;
import cn.zyq.charging.order.pojo.po.ChargingBillFailPO;
import cn.zyq.charging.order.pojo.po.ChargingBillSuccessPO;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import lombok.extern.slf4j.Slf4j;
import java.util.Date;
import java.util.UUID;

/**
 * 设备自检定时任务:
 * (1分钟之内如果设备没有返回执行充电指令成功或失败的消息<emqx.fx客户端没有返回>，则自检失败)
 * 目标 尽快地发现设备故障
 * 如果没有自检任务,设备出现了故障就会走死信队列的代码逻辑,时间会很长。
 * 1分钟的自检时间
 * 加快发现设备故障的时间 尽快的通知用户 充电的用户 换桩 维修的用户 维修充电桩
 *
 * 定时任务
 * 1 按照设置的固定的时间   可以触发一次,也可以触发多次 (可以是某条件下的时间，比如每周二)
 * 2 干一些事情 (在触发器设定的时间点，执行定时任务)
 * Quartz 定时任务框架
 * 有存储概念 默认用的RAM 内存 内存特点 重启后内存的数据就丢失  可以用数据库存储 来进行持久化,
 * 保证我们的订单服务重启,定时任务(也会重启)的数据不会丢失(用mysql持久化orderNo,gunId和OrderRepository)
 * 有集群的概念 cluster  单点故障,
 * 分布式任务 xxl-job
 * https://cloud.tencent.com/developer/article/2335398
 * https://www.oschina.net/p/xxl-job?hmsr=aladdin1e1
 */
@Slf4j
public class DeviceCheckJob  implements  Job{
    /**
     * 必须要定义无参构造器
     * 如果没有无参构造方法,在初始化的时候  newInstance()方法中会调用 getConstructor0 获取构造器进行 初始化
     * (所谓的初始化就是调用构造器)，就会抛出NoSuchMethodException 找不到无参构造的方法异常
     */
    public DeviceCheckJob() {}

    /**
     * 自检任务的构造器 创建任务,设置时间
     * 方法的入参(传入的参数)：
     * @param orderNo 订单号(订单信息)
     * @param gunId 枪id (设备信息)
     */
    public DeviceCheckJob(String orderNo,Integer gunId) throws Exception {
        //一、创建定时任务和调度器
        log.debug("添加设备自检定时任务:订单号-{},枪id-{}",orderNo,gunId);
        //1.创建调度器工厂
        SchedulerFactory sf= new StdSchedulerFactory();
        //2.通过调度器工厂获取调度器
        Scheduler scheduler=sf.getScheduler(); //核心 ☆
        //3.创建定时任务
        JobDetail jobDetail=JobBuilder
                //指定任务类(会自动执行任务类的execute方法)
                .newJob(DeviceCheckJob.class)
                //定义定时任务的名称和组  //任务所属的组名：device_check_job
                .withIdentity("DeviceCheckJob"+ UUID.randomUUID(),"device_check_job")
                //任务中传入订单号
                .usingJobData("orderNo",orderNo)
                //任务中传入充电枪id
                .usingJobData("gunId",gunId)
                .build();


        //二、创建触发器
        //设备自检需要1分钟
        long deviceCheckTime = 1 * 60 * 1000;
        //当前时间
        long now = System.currentTimeMillis();
        //任务触发的时间 (当前时间往后推1分钟) ，1分钟之后触发任务代码的执行。
        Date triggerTime=new Date(now+deviceCheckTime);
        //4.创建触发器 并设置时间
        Trigger trigger=TriggerBuilder.newTrigger().startAt(triggerTime).build();

        //三、用调度器的方法将任务和触发器绑定
        //5.调度器 把 任务和触发器  绑定
        scheduler.scheduleJob(jobDetail, trigger);
        //6.启动调度器
        scheduler.start();
    }


    /**
     * 执行 干一些事情
     * @param jobExecutionContext
     * @throws JobExecutionException
     */
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        log.debug("设备自检任务开始执行:{}",jobExecutionContext);
        //todo 思考给了设备一分钟的时间来自检 一分钟以后,代码会执行到这里, 我们后续的业务该怎么做?
        //startCharging
        //1  设备收到了充电指令并成功执行了开始充电的指令,然后返回开始充电成功指令 ,成功订单表有数据
        //2  设备收到了充电指令且执行开始充电指令失败, 然后返回开始充电失败指令 ,失败订单表有数据
        //3  订单服务没有收到设备返回的任何信息, 成功订单表没有数据,失败订单表没有数据  设备无响应

        // 1.获取任务中存储数据的JobDataMap
        JobDataMap jobDataMap=jobExecutionContext.getJobDetail().getJobDataMap();
        //注： 构造方法中创建JobDetail任务对象时，
        // 用usingJobData("orderNo",orderNo)设置过JobDataMap的数据
        String orderNo=jobDataMap.getString("orderNo");
        int gunId=jobDataMap.getInt("gunId");
        log.debug("获取定时任务上下文数据:{},{},{}",jobDataMap,orderNo,gunId);
        //2.用SpringContextJobUtils类的getBean()方法从spring容器中获取OrderRepository对象。
        //我自己感觉用 @Autowired也可以从spring容器中获取orderRepository对象来使用。
        OrderRepository orderRepository=SpringContextJobUtils.getBean("orderRepository",OrderRepository.class);
        System.err.println("orderRepository:::"+orderRepository);
        //从容器里获取DeviceClient todo 自己实现
        //通过订单号查询成功订单数据 (如果成功订单数据有数据，则设备已经成功执行了开始充电指令，返回开始充电成功指令)
        ChargingBillSuccessPO dbChargingBillSuccessPO=orderRepository.getSuccessOrderByOrderNo(orderNo);//关键代码1，关键代码1，
        //关键代码1，关键代码1，//查询成功订单
        //判断订单成功表有没有数据
        if(dbChargingBillSuccessPO == null){//如果没有成功订单数据
            //就通过订单号获取失败订单数据  //数据库查询是一个很耗资源的操作(尽可能避免查询数据库<没有成功订单再查询是否有失败订单>)
            ChargingBillFailPO dbChargingBillFailPO=orderRepository.getFailOrderByOrderNo(orderNo);//关键代码1，关键代码1，
            //关键代码2，关键代码2，关键代码2，关键代码2，//查询失败订单(如果获取不到成功订单就获取失败订单)
            //判断失败订单表有没有数据
            if(dbChargingBillFailPO==null){
                //如果没有失败订单数据(设备自检的1分钟内，既没有成功订单数据也没有失败订单数据)
                //就给数据库database中插入一条失败订单数据(表示设备自检失败，设备异常)
                ChargingBillFailPO chargingBillFailPO=new ChargingBillFailPO();
                chargingBillFailPO.setBillId(orderNo);
                chargingBillFailPO.setFailDesc("设备自检失败-无响应");
                log.debug("设备自检失败,保存失败订单记录:{}",chargingBillFailPO);
                orderRepository.saveFailOrder(chargingBillFailPO);//关键代码3，关键代码3，
                //关键代码3，关键代码3，关键代码3，关键代码3，//保存失败订单(如果没有成功订单也没有失败订单就保存一个失败订单)
                log.debug("设备自检失败，保存失败订单记录:{} 成功",chargingBillFailPO);


                //修改设备状态为 故障状态  并通知为维修人员
                log.debug("修改设备状态为故障状态：枪id：{}, 状态：{}", gunId, Constant.GUN_STATS_ERROR);
                GunStatusUpdateParam updateGunStatusParam=new GunStatusUpdateParam();
                //设置充电枪状态：充电枪故障(充电枪错误)
                updateGunStatusParam.setStatus(Constant.GUN_STATS_ERROR);
                updateGunStatusParam.setGunId(gunId);

                //从容器里获取DeviceClient:SpringContextJobUtils.getBean("deviceClient",DeviceClient.class)
                //todo 自己实现调用设备服务charging-device服务将charging_gun_info表的充电枪状态字段gun_status
                //更新为故障状态(3)
                //System.err.println("deviceClient::::"+SpringContextJobUtils.getBean("deviceClient",DeviceClient.class));
                DeviceClient deviceClient=SpringContextJobUtils.getBean("deviceClient",DeviceClient.class);///////////////////////////////////////////////////////

                //调用DeviceClient的updateGunStatus方法将充电枪的状态更新为故障状态
                deviceClient.updateGunStatus(updateGunStatusParam);/////////////////////////////////////////////////////////////////////////////////
                //TODO: 通知用户更换充电桩
                log.debug("通知用户设备故障，请您更换充电设备，给您带来的不便，敬请谅解..", gunId, Constant.GUN_STATS_ERROR);
                //TODO: 调用营销系统给用户推送优惠券
                log.debug("调用 营销系统(优惠券服务)给用户发送优惠券(1张价值100元的优惠券已经发送到您的账户中，请注意查收....)");
                //TODO: 通知设备维修人员  //后期可以给项目添加一个保障服务，用于接收订单服务的消息(或者被订单服务调用)来通知维修人员
                //TODO: 后期可以用OpenFeign框架调用保障服务，或者用RabbitMQ给保障服务发送消息，来让保障服务通知维修人员
                log.debug("调用 保障服务 成功，通知维修人员对设备进行检查和维护",gunId,Constant.GUN_STATS_ERROR);

            }else{//如果1分钟自检时间内产生了失败订单数据，就打印"设备自检失败-设备有响应但是充不了电"
                //这里可以表示设备有响应，但是无法充电，比如充电汽车的故障(这种情况也是充电失败)
                log.debug("设备自检失败-设备有响应但是充不了电, (这里的逻辑无需修改设备失败原因)");//关键代码4，关键代码4，
                //todo 后期可以检查失败订单的失败原因描述，如果不是"设备自检失败, 设备有响应，但是充不了电"就通知用户
                log.debug("通知用户设备故障，请用户检查", gunId, Constant.GUN_STATS_ERROR);
            }
        }else{
            log.debug("有订单成功的记录.用Quartz框架模拟的设备自检1分钟---成功,有响应,能充电");
            //把用户id,枪id,运营商id,车辆id,充电站id 更新到订单表
            log.debug("更新设备信息到订单表(这里不是必须的作为演示) 更新枪id:{}到订单成功表 开始...,更新条件是订单编号:{}",gunId,orderNo);
            orderRepository.updateOrderDeviceInfo(orderNo,gunId);//关键代码5，关键代码5，
            //关键代码5，关键代码5，关键代码5，关键代码5，//如果能获取到成功订单就更新成功订单的充电枪id信息
            log.debug("更新设备信息到订单表(这里不是必须的作为演示) 更新枪id:{}到成功订单表 成功!!!,更新条件是订单号:{}",gunId,orderNo);
        }
    }
}
